<?php
/**
 * System module Modules - the module loader and manager.
 *
 * @author Martin "DevelX" Jurča
 */
class Modules extends Module {
    protected $name = 'Modules';
    protected $administrable = true;
    protected $version = '2009-11-08';
    protected $system = true;
    protected $details = array('dependencies' => array(),
            'observes' => array('Modules'));

    /**
     * List of modules.
     *
     * @var Array
     */
    private static $modules = null;

    /**
     * Created instances of modules.
     *
     * @var Array
     */
    private static $moduleInstances = array();

    /**
     * Loaded details about modules. Contains information about module
     * dependences, which modules observes which, localization information
     * and whether the modules are installed.
     *
     * @var Array
     */
    private static $moduleDetials = null;

    /**
     * Contains localized URLs of modules for resolving localized URLs to
     * system names of modules.
     *
     * @var Array
     */
    private static $moduleUrls = null;

    /**
     * DBAPI object for all module constructors.
     *
     * @var DBAPI
     */
    private static $dbapiShare = null;

    /**
     * RecordList API object for all module constructors.
     *
     * @var RecordList
     */
    private static $recordListShare = null;

    /**
     * Lock for getModule method so no module would call the method in it's
     * constructor.
     *
     * @var boolean
     */
    private static $getModuleLock = false;

    public function install() {
        self::loadModulesList();
        self::loadModuleDetails();
        self::loadLocalizedUrls();
        Modules::getModule('Users')->registerRights('Modules',
                $this->getRights());
        return file_exists($this->getDir('/list')) &&
                file_exists($this->getDir('/details')) &&
                file_exists($this->getDir('/urls'));
    }

    public function uninstall() {
        FTP::deleteFile('/modules/Modules/list');
        FTP::deleteFile('/modules/Modules/details');
        unset($_SESSION['Modules_list']);
        unset($_SESSION['Modules_details']);
        return !file_exists($this->getDir('/list')) &&
                !file_exists($this->getDir('/details'));
    }

    public function display($param = null) {
        if (Kernel::getMode() != Kernel::ADMIN) {
            return;
        }
        if (!Modules::getModule('Users')->checkRights('Modules',
                'view admin')) {
            return;
        }
        if ($param == 'submenu') {
            $this->displayAdminSubmenu();
        } else {
            $details = Modules::getModule('View')->getUrl();
            if (count($details) == 1) {
                $this->displayModuleList();
            } elseif (count($details) == 2) {
                $this->displayModuleDetails($details[1]);
            }
        }
    }

    public static function getModule($module) {
        if (self::$getModuleLock) {
            trigger_error("Cannot use method getModule within constructor of" .
                    " module!", E_USER_ERROR);
        }
        if (self::checkModule($module)) {
            if (isset(self::$moduleInstances[$module])) {
                return self::$moduleInstances[$module];
            } else {
                if (self::$moduleDetials == null) {
                    self::loadModuleDetails();
                }
                if (isset(self::$moduleDetials[$module]) &&
                        self::$moduleDetials[$module]['installed']) {
                    require_once Config::ROOT . "/modules/$module/$module.php";
                    self::$getModuleLock = true;
                    self::$moduleInstances[$module] = new $module(
                            self::getDBAPI(), self::getRecordList());
                    self::$getModuleLock = false;
                    return self::$moduleInstances[$module];
                }
                return false;
            }
        }
        return false;
    }

    public static function getModuleList() {
        if (self::$modules == null) {
            self::loadModulesList();
        }
        return self::$modules;
    }

    public static function getModuleDetails($module) {
        if (self::$moduleDetials == null) {
            self::loadModuleDetails();
        }
        if (!isset(self::$moduleDetials[$module])) {
            return false;
        }
        return self::$moduleDetials[$module];
    }

    public static function resolveModuleUrl($url) {
        if (self::$moduleUrls == null) {
            self::loadLocalizedUrls();
        }
        if (isset(self::$moduleUrls[$url])) {
            return self::$moduleUrls[$url];
        }
        return $url;
    }

    public static function getEmergencyDBAPI() {
        return self::getDBAPI();
    }

    public static function &fireEvent($name, &$details, $source = '',
            $target = '', $return = false) {
        if ($name == 'ALL') {
            $name = false;
            return $name;
        }
        $result = array();
        $handler = 'on' . strtoupper(substr($name, 0, 1)) . substr($name, 1);
        if (self::$moduleDetials == null) {
            self::loadModuleDetails();
        }
        foreach (self::$moduleDetials as $module => $detail) {
            if (($target == '') || ($target == $module)) {
                if ($detail['installed']) {
                    if (($source == '') || in_array($source,
                            $detail['observes'])) {
                        $result[$module] = &self::executeEventHandler($module,
                                $handler, $details, $source, '', $return);
                    }
                }
            }
        }
        foreach (self::$moduleDetials as $module => $detail) {
            if ($detail['system'] && in_array('ALL', $detail['observes'])) {
                $result[$module] = &self::executeEventHandler($module, 'onALL',
                        $details, $source, $name, $return);
            }
        }
        return $result;
    }

    public function &onUninstall(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        $result = null;
        if (Modules::getModule('Users')->checkRights('Modules',
                'manage modules')) {
            $moduleName = substr($details, 0, -1);
            $module = Modules::getModule($moduleName);
            if ($module == null) {
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotFound'), View::MSG_WARNING);
                return $result;
            }
            $result = $module->uninstall();
            if ($result === true) {
                if (self::$moduleDetials == null) {
                    self::loadModuleDetails();
                }
                self::$moduleDetials[$moduleName]['installed'] = false;
                self::saveModuleDetails();
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleUninstalled'), View::MSG_NOTICE);
            } elseif (is_string($result)) {
                Modules::getModule('View')->addMessage($result,
                        View::MSG_ERROR);
            } else {
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotUninstalled'), View::MSG_ERROR);
            }
        } else {
            Modules::getModule('View')->addMessage('notSufficentRights',
                    View::MSG_ERROR);
        }
        return $result;
    }

    public function &onInstall(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        $result = null;
        if (Modules::getModule('Users')->checkRights('Modules',
                'manage modules')) {
            $moduleName = substr($details, 0, -1);
            $result = file_exists(Config::ROOT .
                    "/modules/$moduleName/$moduleName.php");
            if ($result === false) {
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotFound'), View::MSG_WARNING);
                return $result;
            }
            require_once Config::ROOT . "/modules/$moduleName/$moduleName.php";
            $module = new $moduleName(self::getDBAPI(), self::getRecordList());
            $result = $module->install();
            if ($result === true) {
                if (self::$moduleDetials == null) {
                    self::loadModuleDetails();
                }
                self::$moduleDetials[$moduleName]['installed'] = true;
                self::saveModuleDetails();
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleInstalled'), View::MSG_NOTICE);
            } elseif (is_string($result)) {
                Modules::getModule('View')->addMessage($result,
                        View::MSG_ERROR);
            } else {
                Modules::getModule('View')->addMessage(
                        self::getDBAPI()->getDriver()->getError(),
                        View::MSG_WARNING);
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotInstalled'), View::MSG_ERROR);
            }
        } else {
            Modules::getModule('View')->addMessage('notSufficentRights',
                    View::MSG_ERROR);
        }
        return $result;
    }

    public function &onReinstall(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        $result = null;
        if (Modules::getModule('Users')->checkRights('Modules',
                'manage modules')) {
            $moduleName = substr($details, 0, -1);
            $result = file_exists(Config::ROOT .
                    "/modules/$moduleName/$moduleName.php");
            if ($result === false) {
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotFound'), View::MSG_WARNING);
                return $result;
            }
            require_once Config::ROOT . "/modules/$moduleName/$moduleName.php";
            $module = new $moduleName(self::getDBAPI(), self::getRecordList());
            $result = $module->uninstall();
            if ($result !== true) {
                $this->displayModuleInstallerError($result);
            } else {
                $result = $module->install();
                if ($result !== true) {
                    $this->displayModuleInstallerError($result);
                } else {
                    Modules::getModule('View')->addMessage(
                            $this->lang('moduleReinstalled'),
                            View::MSG_NOTICE);
                }
            }
        } else {
            Modules::getModule('View')->addMessage('notSufficentRights',
                    View::MSG_ERROR);
        }
        return $result;
    }

    public function &onReload(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        $result = null;
        if (Modules::getModule('Users')->checkRights('Modules',
                'manage modules')) {
            $moduleName = substr($details, 0, -1);
            $result = file_exists(Config::ROOT .
                    "/modules/$moduleName/$moduleName.php");
            if ($result === false) {
                Modules::getModule('View')->addMessage(
                        $this->lang('moduleNotFound'), View::MSG_WARNING);
                return $result;
            }
            require_once Config::ROOT . "/modules/$moduleName/$moduleName.php";
            $module = new $moduleName(self::getDBAPI(), self::getRecordList());
            if (self::$moduleDetials == null) {
                self::loadModuleDetails();
            }
            $details = $module->getDetails();
            $details['installed'] = isset(self::$moduleDetials[$moduleName]) ?
                    self::$moduleDetials[$moduleName]['installed'] : false;
            $details['system'] = $module->isSystem();
            $details['administrable'] = $module->isAdministrable();
            self::$moduleDetials[$moduleName] = $details;
            self::saveModuleDetails();
            if (self::$moduleUrls == null) {
                self::loadLocalizedUrls();
            }
            $urls = $module->getUrls();
            foreach ($urls as $url) {
                self::$moduleUrls[$url] = $moduleName;
            }
            self::saveLocalizedUrls();
            Modules::getModule('View')->addMessage($this->lang('reloaded'),
                    View::MSG_NOTICE);
        } else {
            Modules::getModule('View')->addMessage(
                    $this->lang('notSufficentRights'), View::MSG_ERROR);
        }
        return $result;
    }

    public function &onReloadList(&$details, $source) {
        $result = null;
        Kernel::setRedirect($this->getLink());
        if (!Modules::getModule('Users')->checkRights('Modules',
                'manage modules')) {
            Modules::getModule('View')->addMessage(
                    $this->lang('notSufficentRights'), View::MSG_ERROR);
            return $result;
        }
        $available = glob(Config::ROOT . '/modules/*');
        foreach ($available as &$mod) {
            if (!is_dir($mod)) {
                $mod = '';
            } else {
                $mod = substr($mod, strrpos($mod, '/') + 1);
            }
        }
        if (self::$modules == null) {
            self::loadModulesList();
        }
        if (self::$moduleDetials == null) {
            self::loadModuleDetails();
        }
        $count = count($available);
        $toAdd = array();
        for ($i = 0; $i < $count; $i++) {
            if ($available[$i] != '') {
                if ($this->isAddableModule($available[$i])) {
                    $toAdd[] = $available[$i];
                }
            }
        }
        foreach ($toAdd as $mod) {
            self::$modules[] = $mod;
            self::$moduleDetials[$mod] = array('installed' => false,
                    'system' => false, 'administrable' => false,
                    'dependencies' => array(), 'observes' => array('Modules'),
                    'localization' => array());
        }
        self::saveModulesList();
        self::saveModuleDetails();
        Modules::getModule('View')->addMessage(
                $this->lang('listReloaded'), View::MSG_NOTICE);
        return $result;
    }

    private function isAddableModule($module) {
        foreach (self::$modules as $mod) {
            if ($mod == $module) {
                return false;
            }
        }
        return true;
    }

    /**
     * Displays error message if module installer has failed.
     *
     * @param mixed $result Result of module installer.
     */
    private function displayModuleInstallerError($result) {
        if ($result === true) {
            return;
        }
        if (is_string($result)) {
            Modules::getModule('View')->addMessage($result,
                    View::MSG_ERROR);
        } else {
            Modules::getModule('View')->addMessage(
                    $this->lang('moduleNotInstalled'), View::MSG_ERROR);
        }
    }

    /**
     * Displays administration interface showing module details.
     *
     * @param String $module Module name
     */
    private function displayModuleDetails($module) {
        if (!Modules::getModule('Users')->checkRights('Modules',
                'view module details')) {
            return;
        }
        $details = self::getModuleDetails($module);
        $mod = self::getModule($module);
        if ($mod == null) {
            require_once Config::ROOT . "/modules/$module/$module.php";
            $mod = new $module(self::getDBAPI(), self::getRecordList());
        }
        $templates = glob(Config::ROOT . "/modules/$module/templates/*");
        $templates = $templates ? $templates : array();
        foreach ($templates as &$template) {
            $template = pathinfo($template, PATHINFO_FILENAME);
        }
        $params = array('head' => array(array($this->lang('modulename'),
                    $mod->getUIName())));
        $params['items'] = array(array($this->lang('installedLbl'),
                $this->lang($details['installed'] ? 'yes' : 'no')), array(
                $this->lang('system'), $this->lang($details['system'] ?
                'yes' : 'no')), array($this->lang('administrable'),
                $this->lang($details['administrable'] ? 'yes' : 'no')), array(
                $this->lang('dependencies'), implode(', ',
                $details['dependencies'])), array($this->lang('observes'),
                implode(', ', $details['observes'])), array(
                $this->lang('localization'), implode(', ',
                $details['localization'])), array($this->lang('localName'),
                $mod->getUIName()), array($this->lang('descriptionLbl'),
                $mod->getDescription()), array($this->lang('localUrl'),
                implode(', ', $mod->getUrls())), array($this->lang('version'),
                $mod->getVersion()), array($this->lang('compatible'),
                $mod->getCompatible()), array($this->lang('templates'),
                implode(', ', $templates)));
        $this->template('table', $params);
    }

    /**
     * Displays administration interaface of list of the modules.
     *
     */
    private function displayModuleList() {
        if (!Modules::getModule('Users')->checkRights('Modules',
                'list modules')) {
            return;
        }
        $modules = self::getModuleList();
        $params = array('head' => array(array($this->lang('name'),
                $this->lang('description'), $this->lang('installed'),
                $this->lang('installUninstall'), $this->lang('reinstall'),
                $this->lang('reloadInfo'))));
        $params['items'] = array();
        foreach ($modules as $module) {
            $mod = Modules::getModule($module);
            if ($mod == null) {
                require_once Config::ROOT . "/modules/$module/$module.php";
                $mod = new $module(self::getDBAPI(), self::getRecordList());
            }
            $details = Modules::getModuleDetails($module);
            $item = array('<a href="' . $this->getLink("$module/") . '">' .
                    $mod->getUIName() . '</a>', $mod->getDescription(),
                    $this->lang($details['installed'] ? 'yes' : 'no'),
                    '<a href="' . $this->getLink("$module/",
                    ($details['installed'] ? 'un' : '') . 'install') . '">' .
                    $this->lang(($details['installed'] ? 'un' : '') .
                    'install') . '</a>', '<a href="' .
                    $this->getLink("$module/", 'reinstall') . '">' .
                    $this->lang('reinstall') . '</a>', '<a href="' .
                    $this->getLink("$module/", 'reload') . '">' .
                    $this->lang('reload') . '</a>');
            $params['items'][] = $item;
        }
        $this->template('table', $params);
    }

    /**
     * Displays administration submenu.
     *
     */
    private function displayAdminSubmenu() {
        $params = array('menu' => array(array('href' => $this->getLink(),
                'title' => $this->lang('moduleListTitle'), 'label' =>
                $this->lang('moduleList')), array('href' =>
                $this->getLink('', 'reloadList'), 'title' =>
                $this->lang('reloadTitle'), 'label' =>
                $this->lang('reload'))));
        $this->template('submenu', $params);
    }

    /**
     * Executes event handler of module if the event handler is present.
     *
     * @param String $module Name of the module.
     * @param String $handler Name of the event handler function
     * @param mixed $details Event details.
     * @param String $source Name of the module that fired the event.
     * @param String $event Name of the fired event. Used only with the onALL
     *        event handler.
     * @param boolean $return Whether the caller is interested in return value.
     *        When false, the return value will not be retrieved and the method
     *        will not be executed using the pass-by-reference operator. False
     *        by default.
     * @return mixed Event handler return value.
     */
    private static function &executeEventHandler($module, $handler, &$details,
            $source, $event = '', $return = false) {
        $result = null;
        if ($handler == 'onALL') {
            $mod = self::getModule($module);
            $reflect = new ReflectionObject($mod);
            try {
                $reflect->getMethod('onALL');
                if ($return) {
                    $result = &$mod->onALL($event, $details, $source);
                } else {
                    $mod->onALL($event, $details, $source);
                }
            } catch (Exception $e) {}
        } else {
            $mod = self::getModule($module);
            $reflect = new ReflectionObject($mod);
            try {
                $reflect->getMethod($handler);
                if ($return) {
                    $result = &$mod->{$handler}($details, $source);
                } else {
                    $mod->{$handler}($details, $source);
                }
            } catch (Exception $e) {}
        }
        return $result;
    }

    /**
     * Return the Database API object.
     *
     */
    private static function getDBAPI() {
        if (self::$dbapiShare == null) {
            $driverName = 'DBDriver' . Config::DATABASE_DRIVER;
            $dbdriver = new $driverName(Config::DATABASE_ACCESS);
            self::$dbapiShare = new DBAPI($dbdriver);
        }
        return self::$dbapiShare;
    }

    private static function getRecordList() {
        if (self::$recordListShare == null) {
            self::$recordListShare = new RecordList(self::getDBAPI());
        }
        return self::$recordListShare;
    }

    /**
     * Checks for the presence of module
     *
     * @param String $module Name of the module.
     */
    private static function checkModule($module) {
        if (self::$moduleDetials == null) {
            self::loadModuleDetails();
        }
        if (isset(self::$moduleDetials[$module])) {
            return self::$moduleDetials[$module]['installed'];
        } else {
            if (file_exists(Config::ROOT . "/modules/$module/$module.php")) {
                require_once Config::ROOT . "/modules/$module/$module.php";
                $mod = new $module(self::getDBAPI(), self::getRecordList());
                self::$modules[] = $module;
                self::$moduleDetials[$module] = $mod->getDetails();
                self::$moduleDetials[$module]['installed'] = $mod->isSystem();
                self::$moduleDetials[$module]['system'] = $mod->isSystem();
                self::saveModulesList();
                self::saveModuleDetails();
                return false;
            } else {
                return false;
            }
        }
    }

    /**
     * Saves details about available modules.
     *
     */
    private static function saveModuleDetails() {
        $file = @fopen(Config::ROOT . '/modules/Modules/details', 'w');
        if (!is_resource($file)) {
            return;
        }
        @flock($file, LOCK_EX);
        @fwrite($file, serialize(self::$moduleDetials));
        @flock($file, LOCK_UN);
        @fclose($file);
        $_SESSION['Modules_details'] = self::$moduleDetials;
    }

    /**
     * Loads details about available modules.
     *
     */
    private static function loadModuleDetails() {
        if (isset($_SESSION['Modules_details'])) {
            self::$moduleDetials = $_SESSION['Modules_details'];;
            return;
        }
        if (!file_exists(Config::ROOT . '/modules/Modules/details')) {
            FTP::createFile('/modules/Modules/details');
            $dirs = glob(Config::ROOT . '/modules/*');
            self::$moduleDetials = array();
            foreach ($dirs as $dir) {
                if (is_dir($dir)) {
                    $moduleName = substr($dir, strrpos($dir, '/') + 1);
                    require_once "$dir/$moduleName.php";
                    $module = new $moduleName(self::getDBAPI(),
                            self::getRecordList());
                    $details = $module->getDetails();
                    $details['installed'] = $module->isSystem();
                    $details['system'] = $module->isSystem();
                    $details['administrable'] = $module->isAdministrable();
                    self::$moduleDetials[$moduleName] = $details;
                }
            }
            self::saveModuleDetails();
            return;
        }
        self::$moduleDetials = array();
        $content = @file_get_contents(Config::ROOT .
                '/modules/Modules/details');
        if ($content !== false) {
            self::$moduleDetials = unserialize($content);
        }
    }

    /**
     * Saves list of modules.
     *
     */
    private static function saveModulesList() {
        $file = @fopen(Config::ROOT . '/modules/Modules/list', 'w');
        if (!is_resource($file)) {
            return;
        }
        @flock($file, LOCK_EX);
        @fwrite($file, serialize(self::$modules));
        @flock($file, LOCK_UN);
        @fclose($file);
        $_SESSION['Modules_list'] = self::$modules;
    }

    /**
     * Loads list of modules into $modules static class field.
     *
     */
    private static function loadModulesList() {
        if (isset($_SESSION['Modules_list'])) {
            self::$modules = $_SESSION['Modules_list'];
            return;
        }
        if (!file_exists(Config::ROOT . '/modules/Modules/list')) {
            FTP::createFile('/modules/Modules/list');
            $dirs = glob(Config::ROOT . '/modules/*');
            self::$modules = array();
            foreach ($dirs as $dir) {
                if (is_dir($dir)) {
                    self::$modules[] = substr($dir, strrpos($dir, '/') + 1);
                }
            }
            self::saveModulesList();
            return;
        }
        self::$modules = array();
        $content = @file_get_contents(Config::ROOT . '/modules/Modules/list');
        if ($content !== false) {
            self::$modules = unserialize($content);
        }
    }

    /**
     * Saves cached modules' localized URLs to file.
     *
     */
    private static function saveLocalizedUrls() {
        $file = @fopen(Config::ROOT . '/modules/Modules/urls', 'w');
        if (!is_resource($file)) {
            return;
        }
        @flock($file, LOCK_EX);
        @fwrite($file, serialize(self::$moduleUrls));
        @flock($file, LOCK_UN);
        @fclose($file);
        $_SESSION['Modules_urls'] = self::$moduleUrls;
    }

    /**
     * Load localized URLs of modules.
     *
     */
    private static function loadLocalizedUrls() {
        if (isset($_SESSION['Modules_urls'])) {
            self::$moduleUrls = $_SESSION['Modules_urls'];
            return;
        }
        if (!file_exists(Config::ROOT . '/modules/Modules/urls')) {
            FTP::createFile('/modules/Modules/urls');
            $modules = self::getModuleList();
            self::$moduleUrls = array();
            foreach ($modules as $module) {
                require_once Config::ROOT . "/modules/$module/$module.php";
                $mod = new $module(self::getDBAPI(), self::getRecordList());
                $urls = $mod->getUrls();
                foreach ($urls as $url) {
                    self::$moduleUrls[$url] = $module;
                }
            }
            self::saveLocalizedUrls();
            return;
        }
        self::$moduleUrls = array();
        $content = @file_get_contents(Config::ROOT . '/modules/Modules/urls');
        if ($content !== false) {
            self::$moduleUrls = unserialize($content);
        }
    }
}
?>
